package utils;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.bind.*;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.stream.StreamSource;
import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class Parser {

    public String toXml(Object obj) {
        ByteArrayOutputStream bs = new ByteArrayOutputStream();
        write(obj, bs);
        return new String(bs.toByteArray());
    }

    public <T> T formXml(Class<T> clazz, String xml) {
        ByteArrayInputStream is;
        try {
            is = new ByteArrayInputStream(xml.getBytes("UTF-8"));
            return fromXml(clazz, is);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    public <T> T fromXml(Class<T> clazz, InputStream inputStream) {
        try {
            JAXBContext context = getContext(clazz);

            Unmarshaller unmarshaller = context.createUnmarshaller();
            JAXBElement<T> element = unmarshaller.unmarshal(new StreamSource(inputStream), clazz);
            return element.getValue();

        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
    }

    public <T> List<T> fromXmlList(Class<T> clazz, String xml) {
        try {
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            documentBuilderFactory.setNamespaceAware(true);

            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            Document document = documentBuilder.parse(new InputSource(new StringReader("<root>" + xml + "</root>")));

            JAXBContext context = getContext(clazz);
            Unmarshaller unmarshaller = context.createUnmarshaller();

            List<T> result = new ArrayList<T>();
            NodeList nodeList = document.getDocumentElement().getChildNodes();
            for (int i = 0; i < nodeList.getLength(); i++) {
                Node node = nodeList.item(i);
                if (node.getNodeType() != Node.ELEMENT_NODE) continue;

                JAXBElement<T> element = unmarshaller.unmarshal(node, clazz);
                result.add(element.getValue());
            }

            return result;

        } catch (ParserConfigurationException e) {
            throw new IllegalStateException(e);
        } catch (SAXException e) {
            throw new DataBindingException(e);
        } catch (IOException e) {
            throw new DataBindingException(e);
        } catch (JAXBException e) {
            throw new DataBindingException(e);
        }
    }

    private void write(Object obj, OutputStream stream) {
        try {
            JAXBContext context;
            if (obj instanceof JAXBElement) {
                JAXBElement jaxbElement = (JAXBElement) obj;
                context = getContext(jaxbElement.getValue().getClass());
            } else {
                context = getContext(obj.getClass());
            }

            Marshaller marshaller = context.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
            marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);

            marshaller.marshal(obj, stream);

        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
    }

    // todo: cache can be added
    private JAXBContext getContext(Class clazz) throws JAXBException {
        return JAXBContext.newInstance(clazz);
    }
}